﻿using System;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Text;
using System.Xml.Linq;
using FireBus.BuildTasks.Properties;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;


namespace FireBus.BuildTasks
{
	public class FxCop : Task
	{
		private readonly String _CurrentDirectory = Directory.GetCurrentDirectory().ToUpperInvariant().TrimEnd('\\') + "\\";

		private readonly Decoder _Decoder = new Decoder();


		[Required]
		public ITaskItem[] Assemblies { get; set; }

		public ITaskItem Dictionary { get; set; }

		public ITaskItem Executable { get; set; }

		public Boolean FailOnError { get; set; }

		public Boolean FailOnWarning { get; set; }

		public ITaskItem Output { get; set; }

		public String RuleSet { get; set; }

		public ITaskItem RuleSetDirectory { get; set; }


		public FxCop()
		{
			FailOnError = true;
			FailOnWarning = false;
			RuleSet = "AllRules.ruleset";

			// Pre-fill information from Visual Studio 2010, if available
			var vs10ToolsPath = Environment.GetEnvironmentVariable("VS100COMNTOOLS");
			if (vs10ToolsPath != null)
			{
				String executable = Path.Combine(vs10ToolsPath, @"..\..\Team Tools\Static Analysis Tools\FxCop\FxCopCmd.exe");
				if (File.Exists(executable))
				{
					Executable = new TaskItem(executable);
				}

				String ruleSetDirectory = Path.Combine(vs10ToolsPath, @"..\..\Team Tools\Static Analysis Tools\Rule Sets");
				if (Directory.Exists(ruleSetDirectory))
				{
					RuleSetDirectory = new TaskItem(ruleSetDirectory);
				}
			}
		}

		public override Boolean Execute()
		{
			if (Executable == null)
			{
				Log.LogError(
					Resources.EXECUTABLE_WAS_NOT_SET);
				return false;
			}
			if (RuleSetDirectory == null)
			{
				Log.LogError(
					Resources.RULESETDIRECTORY_WAS_NOT_SET);
				return false;
			}

			var deleteOutput = false;

			if (Output == null)
			{
				deleteOutput = true;
				Output = new TaskItem(Path.GetTempFileName());
			}
			else
			{
				var outputFilePath = Output.GetMetadata("FullPath");
				var outputDirectory = Path.GetDirectoryName(outputFilePath);
				if (String.IsNullOrEmpty(outputDirectory))
				{
					Log.LogError(
						Resources.INVALID_FXCOP_OUTPUT_FILE_PATH);
					return false;
				}

				if (!Directory.Exists(outputDirectory))
				{
					try
					{
						Directory.CreateDirectory(outputDirectory);
					}
					catch(Exception)
					{
						Log.LogError(
							Resources.CANT_CREATE_DIRECTORY_FOR_FXCOP_OUTPUT);
						throw;
					}
				}
			}

			try
			{
				foreach (ITaskItem assembly in Assemblies)
				{
					Log.LogMessage(
						MessageImportance.High,
						Resources.RUNNING_FXCOP_ON_FILEPATH,
						GetRelativePath(assembly.GetMetadata("FullPath")));
				}

				using (var proc = CreateProcess())
				{
					proc.Start();
					proc.WaitForExit();

					if (proc.ExitCode != 0)
					{
						LogExecutionFailure(proc);
						return false;
					}
				}

				return ParseXml();
			}
			finally
			{
				if (deleteOutput)
				{
					File.Delete(Output.GetMetadata("FullPath"));
				}
			}
		}

		// Helpers

		private Process CreateProcess()
		{
			var args = new StringBuilder();
			args.AppendFormat(
				@"/q /iit /fo /igc /gac /rs:""={0}"" /rsd:""{1}"" /o:""{2}""",
				RuleSet,
				RuleSetDirectory.GetMetadata("FullPath"),
				Output.GetMetadata("FullPath"));
			if (Dictionary != null)
			{
				args.AppendFormat(
					@" /dic:""{0}""",
					Dictionary.GetMetadata("FullPath"));
			}
			foreach (var assembly in Assemblies)
			{
				args.AppendFormat(
					@" /f:""{0}""",
					assembly.GetMetadata("FullPath"));
			}

			var startInfo =
				new ProcessStartInfo
				{
					UseShellExecute = false,
					CreateNoWindow = true,
					WindowStyle = ProcessWindowStyle.Hidden,
					RedirectStandardOutput = true,
					RedirectStandardError = true,
					FileName = Executable.GetMetadata("FullPath"),
					Arguments = args.ToString()
				};
			var process =
				new Process
					{
						StartInfo = startInfo
					};
			return process;
		}

		private static String GetErrorCode(XElement xml)
		{
			var id = (String)xml.Attribute("CheckId");
			var category = (String)xml.Attribute("Category");
			return String.Format(
				CultureInfo.CurrentCulture,
				"{0} ({1})",
				id,
				category);
		}

		private String GetFilename(String assembly, XElement xml)
		{
			var result = assembly;
			var path = (String)xml.Attribute("Path");
			var file = (String)xml.Attribute("File");

			if (path != null && file != null)
			{
				result = Path.Combine(path, file);
			}

			return GetRelativePath(result);
		}

		private static String GetLevel(XElement xml)
		{
			return (String)xml.Attribute("Level");
		}

		private static Int32 GetLineNumber(XElement xml)
		{
			var line = (String)xml.Attribute("Line");
			return line == null ? 0 : Int32.Parse(line, CultureInfo.InvariantCulture);
		}

		private String GetMessage(XElement xml)
		{
			return _Decoder.Decode(
				xml.FirstNode.ToString().Replace("\r\n", "\n"));
		}

		private String GetRelativePath(String path)
		{
			if (path.ToUpperInvariant().StartsWith(_CurrentDirectory, false, CultureInfo.CurrentCulture))
			{
				path = path.Substring(_CurrentDirectory.Length);
			}

			return path;
		}

		private void LogExecutionFailure(Process proc)
		{
			String line;
			while ((line = proc.StandardOutput.ReadLine()) != null)
			{
				Log.LogMessage(MessageImportance.High, line);
			}
			while ((line = proc.StandardError.ReadLine()) != null)
			{
				Log.LogMessage(MessageImportance.High, line);
			}

			Log.LogError(
				Resources.FXCOP_EXITED_WITH_ERROR_CODE_N,
				proc.ExitCode);
		}

		private Boolean ParseXml()
		{
			var hasErrors = false;
			var hasWarnings = false;
			MessageLogger errorLogger = (errorCode, file, lineNumber, message) => Log.LogError("FxCop", errorCode, null, file, lineNumber, 0, 0, 0, message);
			MessageLogger warningLogger = (errorCode, file, lineNumber, message) => Log.LogWarning("FxCop", errorCode, null, file, lineNumber, 0, 0, 0, message);
			var root = XElement.Load(Output.GetMetadata("FullPath"));

			foreach (XElement targetXml in root.Descendants("Target"))
			{
				var assembly = (String)targetXml.Attribute("Name");

				foreach (XElement messageXml in targetXml.Descendants("Message"))
				{
					String errorCode = GetErrorCode(messageXml);

					foreach (XElement issueXml in messageXml.Descendants("Issue"))
					{
						String level = GetLevel(issueXml);
						MessageLogger logger;

						if (level.EndsWith("ERROR", StringComparison.OrdinalIgnoreCase))
						{
							logger = FailOnError ? errorLogger : warningLogger;
							hasErrors = true;
						}
						else if (level.EndsWith("WARNING", StringComparison.OrdinalIgnoreCase))
						{
							logger = FailOnWarning ? errorLogger : warningLogger;
							hasWarnings = true;
						}
						else
						{
							logger = warningLogger;
						}

						var file = GetFilename(assembly, issueXml);
						var message = GetMessage(issueXml);
						var lineNumber = GetLineNumber(issueXml);

						logger(errorCode, file, lineNumber, message);
					}
				}
			}

			return (!((hasErrors && FailOnError) || (hasWarnings && FailOnWarning)));
		}
	}
}
